// Copyright (c) 2018-2023 The MobileCoin Foundation

//! MobileNode Internal Enclave Implementation
//!
//! This crate implements the inside-the-enclave version of the EnclaveAPI,
//! which would traditionally be inside the enclave crate. This, combined
//! with a form of dependency injection, would provide the machines with
//! all the unit testing they would ever need. Fate, it seems, has a sense
//! of irony...

#![no_std]
#![allow(clippy::result_large_err)]

extern crate alloc;

mod constant_time_division;
mod constant_time_token_map;
mod identity;

// Include autogenerated constants.rs
include!(concat!(env!("OUT_DIR"), "/constants.rs"));

use alloc::{
    boxed::Box,
    collections::BTreeSet,
    format,
    string::{String, ToString},
    vec,
    vec::Vec,
};
use constant_time_division::ct_u64_divide;
use constant_time_token_map::CtTokenMap;
use core::cmp::min;
use identity::Ed25519Identity;
use mc_account_keys::PublicAddress;
use mc_attest_core::{
    DcapEvidence, EnclaveReportDataContents, EvidenceKind, IntelSealed, Report, TargetInfo,
};
use mc_attest_enclave_api::{
    ClientAuthRequest, ClientAuthResponse, ClientSession, EnclaveMessage,
    Error as AttestEnclaveError, PeerAuthRequest, PeerAuthResponse, PeerSession,
};
use mc_attest_trusted::SealAlgo;
use mc_blockchain_types::{Block, BlockContents, BlockSignature, BlockVersion};
use mc_common::{
    logger::{log, Logger},
    ResponderId,
};
use mc_consensus_enclave_api::{
    BlockchainConfig, BlockchainConfigWithDigest, ConsensusEnclave, Error, FeePublicKey,
    FormBlockInputs, LocallyEncryptedTx, Result, SealedBlockSigningKey, TxContext,
    WellFormedEncryptedTx, WellFormedTxContext,
};
use mc_crypto_ake_enclave::AkeEnclaveState;
use mc_crypto_digestible::{DigestTranscript, Digestible, MerlinTranscript};
use mc_crypto_keys::{Ed25519Pair, Ed25519Public, RistrettoPrivate, RistrettoPublic, X25519Public};
use mc_crypto_message_cipher::{AesMessageCipher, MessageCipher};
use mc_rand::McRng;
use mc_sgx_compat::sync::Mutex;
use mc_sgx_report_cache_api::{ReportableEnclave, Result as ReportableEnclaveResult};
use mc_transaction_core::{
    encrypted_fog_hint::EncryptedFogHint,
    membership_proofs::compute_implied_merkle_root,
    mint::{
        validate_mint_config_tx, validate_mint_tx, MintConfig, MintConfigTx, MintTx,
        MintValidationError, ValidatedMintConfigTx,
    },
    ring_signature::{KeyImage, Scalar},
    tokens::Mob,
    tx::{Tx, TxOut, TxOutMembershipElement, TxOutMembershipProof},
    validation::TransactionValidationError,
    Amount, Token, TokenId, SMALLEST_MINIMUM_FEE_LOG2,
};
// Race here refers to, this is thread-safe, first-one-wins behavior, without
// blocking
use once_cell::race::OnceBox;
use prost::Message;
use rand_core::{CryptoRng, RngCore};

/// Domain separator for unified fees transaction private key.
pub const FEES_OUTPUT_PRIVATE_KEY_DOMAIN_TAG: &str = "mc_fees_output_private_key";

/// Domain separator for minted txouts public keys.
pub const MINTED_OUTPUT_PRIVATE_KEY_DOMAIN_TAG: &str = "mc_minted_output_private_key";

include!(concat!(env!("OUT_DIR"), "/target_features.rs"));

/// A well-formed transaction.
#[derive(Clone, Eq, PartialEq, Message)]
pub struct WellFormedTx {
    /// The actual transaction.
    #[prost(message, required, tag = "1")]
    tx: Tx,
}

impl WellFormedTx {
    pub fn tx(&self) -> &Tx {
        &self.tx
    }
}

impl From<Tx> for WellFormedTx {
    fn from(tx: Tx) -> Self {
        Self { tx }
    }
}

/// A list of transactions.
///
/// This is the contents of the encrypted payload
/// returned by `txs_for_peer` and fed into `peer_tx_propose`.
/// We need to define this since that's the only way to get Prost to serialize a
/// list of transactions. Prost is used for the sake of uniformity - all other
/// data inside `consensus_enclave_impl` is also serialized using it.
#[derive(Message)]
pub struct TxList {
    /// Transactions.
    #[prost(message, repeated, tag = "1")]
    pub txs: Vec<Tx>,
}

/// Internal state of the enclave, including AKE and attestation related as well
/// as any business logic state.
pub struct SgxConsensusEnclave {
    /// All AKE and attestation related state including responder ids,
    /// established channels for peers and clients, and any pending quotes
    /// or attestation evidence.
    ake: AkeEnclaveState<Ed25519Identity>,

    /// Cipher used to encrypt locally-cached transactions.
    locally_encrypted_tx_cipher: Mutex<AesMessageCipher>,

    /// Cipher used to encrypt well-formed-encrypted transactions.
    well_formed_encrypted_tx_cipher: Mutex<AesMessageCipher>,

    /// Logger.
    logger: Logger,

    /// Blockchain Config
    ///
    /// This is configuration data that affects whether or not a transaction
    /// is valid. To ensure that it is uniform across the network, it's hash
    /// gets appended to responder id.
    blockchain_config: OnceBox<BlockchainConfigWithDigest>,

    /// Constant time minimum fee map, initialized from blockchain config
    ct_min_fee_map: OnceBox<CtTokenMap<u64>>,
}

impl SgxConsensusEnclave {
    pub fn new(logger: Logger) -> Self {
        Self {
            ake: Default::default(),
            locally_encrypted_tx_cipher: Mutex::new(AesMessageCipher::new(&mut McRng)),
            well_formed_encrypted_tx_cipher: Mutex::new(AesMessageCipher::new(&mut McRng)),
            logger,
            blockchain_config: Default::default(),
            ct_min_fee_map: Default::default(),
        }
    }

    fn encrypt_well_formed_tx<R: RngCore + CryptoRng>(
        &self,
        well_formed_tx: &WellFormedTx,
        rng: &mut R,
    ) -> Result<WellFormedEncryptedTx> {
        let well_formed_tx_bytes = mc_util_serial::encode(well_formed_tx);

        Ok(WellFormedEncryptedTx(
            self.well_formed_encrypted_tx_cipher
                .lock()?
                .encrypt_bytes(rng, well_formed_tx_bytes),
        ))
    }

    // Get a WellFormedTxContext for a Tx, given the minimum fee for its specified
    // fee token.
    fn get_well_formed_tx_context(&self, tx: &Tx, min_fee: u64) -> WellFormedTxContext {
        // Compute the priority number to assign to this tx.
        //
        // Because different tokens have different minimum fees, and users usually
        // use the minimum fee, we want to normalize so that the priority of any
        // two payments using the minimum fee is exactly the same number.
        //
        // We also want to allow that increments of ~1% of the minimum fee actually
        // affect the priority of your payment. So, we divide the min fee by 128,
        // before dividing the fee by this. Separately, the fee map enforces that
        // the minimum fees are >= SMALLEST_MINIMUM_FEE, and so we are not dividing by
        // zero.
        let (priority, _) = ct_u64_divide(tx.prefix.fee, min_fee >> SMALLEST_MINIMUM_FEE_LOG2);

        WellFormedTxContext::from_tx(tx, priority)
    }

    fn decrypt_well_formed_tx(&self, encrypted: &WellFormedEncryptedTx) -> Result<WellFormedTx> {
        let mut cipher = self.well_formed_encrypted_tx_cipher.lock()?;
        let plaintext = cipher.decrypt_bytes(encrypted.0.clone())?;
        let well_formed_tx: WellFormedTx = mc_util_serial::decode(&plaintext)?;
        Ok(well_formed_tx)
    }

    /// Given a list of well formed encrypted txs + proofs and a root membership
    /// element, decrypt and validate "original" Tx transactions, and if
    /// successful return the list of transactions.
    fn get_txs_from_inputs(
        &self,
        well_formed_encrypted_txs_with_proofs: &[(
            WellFormedEncryptedTx,
            Vec<TxOutMembershipProof>,
        )],
        parent_block: &Block,
        root_element: &TxOutMembershipElement,
        config: &BlockchainConfig,
        ct_min_fees: &CtTokenMap<u64>,
        rng: &mut (impl RngCore + CryptoRng),
    ) -> Result<Vec<Tx>> {
        // Short-circuit if there are no transactions.
        if well_formed_encrypted_txs_with_proofs.is_empty() {
            return Ok(Vec::new());
        }

        // This implicitly converts Vec<Result<(Tx Vec<TxOutMembershipProof>),_>> into
        // Result<Vec<(Tx, Vec<TxOutMembershipProof>)>, _>, and terminates the
        // iteration when the first Error is encountered.
        let transactions_with_proofs = well_formed_encrypted_txs_with_proofs
            .iter()
            .map(|(encrypted_tx, proofs)| {
                Ok((
                    self.decrypt_well_formed_tx(encrypted_tx)?.tx,
                    proofs.clone(),
                ))
            })
            .collect::<Result<Vec<(Tx, Vec<TxOutMembershipProof>)>>>()?;

        // root_elements contains the root hash of the Merkle tree of all TxOuts in the
        // ledger that were used to validate the transactions.
        let mut root_elements = Vec::new();

        // We need to make sure all transactions are valid. We also ensure they all
        // point at the same root membership element.
        for (tx, proofs) in transactions_with_proofs.iter() {
            let fee_token_id = TokenId::from(tx.prefix.fee_token_id);

            let minimum_fee = ct_min_fees
                .get(&fee_token_id)
                .ok_or(TransactionValidationError::TokenNotYetConfigured)?;

            mc_transaction_core::validation::validate(
                tx,
                parent_block.index + 1,
                config.block_version,
                proofs,
                minimum_fee,
                rng,
            )?;

            for proof in proofs {
                let root_element = compute_implied_merkle_root(proof)
                    .map_err(|_e| TransactionValidationError::InvalidLedgerContext)?;
                root_elements.push(root_element);
            }
        }

        root_elements.sort();
        root_elements.dedup();

        if root_elements.len() != 1 {
            return Err(Error::InvalidLocalMembershipProof);
        }

        // Sanity check - since our caller (ByzantineLedgerWorker/TxManager) collects
        // all proof of memberships and root element at a time the ledger is not
        // expected to change, we should end with the same root element for all
        // transactions.
        if root_element != &root_elements[0] {
            return Err(Error::InvalidLocalMembershipRootElement);
        }

        let transactions: Vec<Tx> = transactions_with_proofs
            .into_iter()
            .map(|(tx, _proofs)| tx)
            .collect();

        // Duplicate transactions are not allowed.
        // This check is redundant with the duplicate key image check, but might be
        // helpful for early debugging.
        let mut tx_hashes = BTreeSet::new();
        for tx in &transactions {
            let tx_hash = tx.tx_hash();
            if tx_hashes.contains(&tx_hash) {
                return Err(Error::FormBlock(format!(
                    "Duplicate transaction: {tx_hash}"
                )));
            }
            tx_hashes.insert(tx_hash);
        }

        // Duplicate key images are not allowed.
        let mut used_key_images = BTreeSet::default();
        for tx in &transactions {
            for key_image in tx.key_images() {
                if used_key_images.contains(&key_image) {
                    return Err(Error::FormBlock(format!(
                        "Duplicate key image: {key_image:?}"
                    )));
                }
                used_key_images.insert(key_image);
            }
        }

        // Duplicate output public keys are not allowed.
        let mut seen_output_public_keys = BTreeSet::default();
        for tx in &transactions {
            for public_key in tx.output_public_keys() {
                if seen_output_public_keys.contains(&public_key) {
                    return Err(Error::FormBlock(format!(
                        "Duplicate output public key: {public_key:?}"
                    )));
                }
                seen_output_public_keys.insert(public_key);
            }
        }

        Ok(transactions)
    }

    /// Validate a list of MintConfigTxs.
    ///
    /// # Arguments
    /// * `mint_config_txs` - The [MintConfigTx] objects to validate.
    /// * `current_block_index` - The index of the current block that is being
    ///   built. See documentation of
    ///   [mc_transaction_core::mint::validate_mint_config_tx] for more details
    ///   on why this is optional.
    /// * `config` - The current blockchain configuration.
    fn validate_mint_config_txs(
        mint_config_txs: Vec<MintConfigTx>,
        current_block_index: Option<u64>,
        config: &BlockchainConfig,
    ) -> Result<Vec<ValidatedMintConfigTx>> {
        let mut seen_nonces = BTreeSet::default();
        let mut validated_txs = Vec::with_capacity(mint_config_txs.len());

        for tx in mint_config_txs {
            // Ensure all nonces are unique.
            if !seen_nonces.insert((tx.prefix.nonce.clone(), tx.prefix.token_id)) {
                return Err(Error::FormBlock(format!(
                    "Duplicate MintConfigTx nonce for token id {}: {:?}",
                    tx.prefix.token_id, tx.prefix.nonce
                )));
            }

            // Get the governors for the token being minted.
            let token_id = TokenId::from(tx.prefix.token_id);
            let governors = config
                .governors_map
                .get_governors_for_token(&token_id)
                .ok_or(Error::MalformedMintingTx(MintValidationError::NoGovernors(
                    token_id,
                )))?;

            // Ensure transaction is valid.
            validate_mint_config_tx(&tx, current_block_index, config.block_version, &governors)?;

            validated_txs.push(ValidatedMintConfigTx {
                mint_config_tx: tx,
                signer_set: governors,
            });
        }

        Ok(validated_txs)
    }

    /// Validate a list of MintTxs.
    fn validate_mint_txs(
        mint_txs_with_config: Vec<(MintTx, MintConfigTx, MintConfig)>,
        current_block_index: u64,
        config: &BlockchainConfig,
    ) -> Result<Vec<MintTx>> {
        // Ensure all nonces are unique and all transactions matches the associated
        // configuration.
        let mut mint_txs = Vec::with_capacity(mint_txs_with_config.len());
        let mut seen_nonces = BTreeSet::default();
        for (mint_tx, mint_config_tx, mint_config) in mint_txs_with_config {
            // The nonce should be unique.
            if !seen_nonces.insert((mint_tx.prefix.nonce.clone(), mint_tx.prefix.token_id)) {
                return Err(Error::FormBlock(format!(
                    "Duplicate MintTx nonce for token id {}: {:?}",
                    mint_tx.prefix.token_id, mint_tx.prefix.nonce
                )));
            }

            // The associated MintConfigTx should be for the same token being minted.
            if mint_config_tx.prefix.token_id != mint_tx.prefix.token_id {
                return Err(Error::FormBlock(format!(
                    "MintTx token_id does not match MintConfigTx token_id: {:?} != {:?}",
                    mint_tx.prefix.token_id, mint_config_tx.prefix.token_id
                )));
            }

            // The specific MintConfig used should be part of the provided MintConfigTx.
            if !mint_config_tx.prefix.configs.contains(&mint_config) {
                return Err(Error::FormBlock(format!(
                    "MintTx {mint_tx} referenced a MintConfig that is not part of the MintConfigTx",
                )));
            }

            // The associated MintConfigTx should be valid.
            // No block index is passed since the MintConfigTx is already assumed to be in
            // the ledger, so doing the tombstone check is pointless (and could
            // fail if enough blocks have passed since the MintConfigTx got
            // accepted).
            Self::validate_mint_config_txs(vec![mint_config_tx], None, config)?;

            // The MintTx should be valid.
            validate_mint_tx(
                &mint_tx,
                current_block_index,
                config.block_version,
                &mint_config,
            )?;

            // MintTx is valid.
            mint_txs.push(mint_tx);
        }

        Ok(mint_txs)
    }
}

impl ReportableEnclave for SgxConsensusEnclave {
    fn new_ereport(
        &self,
        qe_info: TargetInfo,
    ) -> ReportableEnclaveResult<(Report, EnclaveReportDataContents)> {
        Ok(self.ake.new_ereport(qe_info)?)
    }

    fn verify_attestation_evidence(
        &self,
        attestation_evidence: DcapEvidence,
    ) -> ReportableEnclaveResult<()> {
        self.ake.verify_attestation_evidence(attestation_evidence)?;
        Ok(())
    }

    fn get_attestation_evidence(&self) -> ReportableEnclaveResult<DcapEvidence> {
        Ok(self.ake.get_attestation_evidence()?)
    }
}

impl ConsensusEnclave for SgxConsensusEnclave {
    fn enclave_init(
        &self,
        peer_self_id: &ResponderId,
        client_self_id: &ResponderId,
        sealed_key: &Option<SealedBlockSigningKey>,
        blockchain_config: BlockchainConfig,
    ) -> Result<(SealedBlockSigningKey, Vec<String>)> {
        // Ensure the blockchain configuration is valid.
        let minting_trust_root_public_key =
            Ed25519Public::try_from(&MINTING_TRUST_ROOT_PUBLIC_KEY[..])
                .map_err(Error::ParseMintingTrustRootPublicKey)?;

        blockchain_config.validate(&minting_trust_root_public_key)?;

        // Set the minimum fee map.
        self.ct_min_fee_map
            .set(Box::new(
                blockchain_config.fee_map.as_ref().iter().collect(),
            ))
            .expect("enclave was already initialized");

        // Inject the configuration into the peer ResponderId.
        let blockchain_config = BlockchainConfigWithDigest::from(blockchain_config);
        let peer_self_id = blockchain_config.responder_id(peer_self_id);

        self.blockchain_config
            .set(Box::new(blockchain_config))
            .expect("enclave already initialized");

        // Init AKE.
        self.ake.init(peer_self_id, client_self_id.clone())?;

        // If we were passed a sealed key, unseal it and overwrite the private key.
        if let Some(sealed) = sealed_key {
            log::trace!(self.logger, "trying to unseal key");
            let cached = IntelSealed::try_from(sealed.clone())?;
            let (key, _mac) = cached.unseal_raw()?;
            let mut lock = self.ake.get_identity().signing_keypair.lock().unwrap();
            *lock = Ed25519Pair::try_from(&key[..])?;
        }

        // Either way seal the private key and return it.
        let lock = self.ake.get_identity().signing_keypair.lock().unwrap();
        let key = (*lock).private_key();
        let sealed = IntelSealed::seal_raw(key.as_ref(), &[])?;

        Ok((
            sealed.as_ref().to_vec(),
            TARGET_FEATURES
                .iter()
                .map(|feature| String::from(*feature))
                .collect::<Vec<String>>(),
        ))
    }

    fn get_minimum_fee(&self, token_id: &TokenId) -> Result<Option<u64>> {
        Ok(self
            .blockchain_config
            .get()
            .ok_or(Error::NotInitialized)?
            .get_config()
            .fee_map
            .get_fee_for_token(token_id))
    }

    fn get_identity(&self) -> Result<X25519Public> {
        Ok(self.ake.get_kex_identity())
    }

    fn get_signer(&self) -> Result<Ed25519Public> {
        Ok(self.ake.get_identity().get_public_key())
    }

    fn get_fee_recipient(&self) -> Result<FeePublicKey> {
        let spend_public_key = RistrettoPublic::try_from(&FEE_SPEND_PUBLIC_KEY).map_err(|e| {
            Error::FeePublicAddress(format!("Could not get fee spend public: {e:?}"))
        })?;
        let view_public_key = RistrettoPublic::try_from(&FEE_VIEW_PUBLIC_KEY).map_err(|e| {
            Error::FeePublicAddress(format!("Could not get fee view public: {e:?}"))
        })?;
        Ok(FeePublicKey {
            spend_public_key,
            view_public_key,
        })
    }

    fn get_minting_trust_root(&self) -> Result<Ed25519Public> {
        Ed25519Public::try_from(&MINTING_TRUST_ROOT_PUBLIC_KEY[..])
            .map_err(Error::ParseMintingTrustRootPublicKey)
    }

    fn client_accept(&self, req: ClientAuthRequest) -> Result<(ClientAuthResponse, ClientSession)> {
        Ok(self.ake.client_accept(req)?)
    }

    fn client_close(&self, channel_id: ClientSession) -> Result<()> {
        Ok(self.ake.client_close(channel_id)?)
    }

    fn client_discard_message(&self, msg: EnclaveMessage<ClientSession>) -> Result<()> {
        let _ = self.ake.client_decrypt(msg)?;
        Ok(())
    }

    fn peer_init(&self, peer_id: &ResponderId) -> Result<PeerAuthRequest> {
        // Inject the blockchain config hash, passing off to the AKE
        let peer_id = self
            .blockchain_config
            .get()
            .ok_or(Error::NotInitialized)?
            .responder_id(peer_id);

        Ok(self.ake.peer_init(&peer_id)?)
    }

    fn peer_accept(&self, req: PeerAuthRequest) -> Result<(PeerAuthResponse, PeerSession)> {
        Ok(self.ake.peer_accept(req)?)
    }

    fn peer_connect(
        &self,
        peer_id: &ResponderId,
        msg: PeerAuthResponse,
    ) -> Result<(PeerSession, EvidenceKind)> {
        // Inject the blockchain config hash before passing off to the AKE
        let peer_id = self
            .blockchain_config
            .get()
            .ok_or(Error::NotInitialized)?
            .responder_id(peer_id);

        Ok(self.ake.peer_connect(&peer_id, msg)?)
    }

    fn peer_close(&self, session_id: &PeerSession) -> Result<()> {
        Ok(self.ake.peer_close(session_id)?)
    }

    fn client_tx_propose(&self, msg: EnclaveMessage<ClientSession>) -> Result<TxContext> {
        let tx_bytes = self.ake.client_decrypt(msg)?;

        // Try and deserialize.
        let tx: Tx = mc_util_serial::decode(&tx_bytes)?;

        // Verify fee map digest if it is present.
        if !tx.fee_map_digest.is_empty()
            && tx.fee_map_digest[..]
                != self
                    .blockchain_config
                    .get()
                    .ok_or(Error::NotInitialized)?
                    .canonical_fee_map_digest()[..]
        {
            return Err(Error::FeeMapDigestMismatch);
        }

        // Convert to TxContext
        let maybe_locally_encrypted_tx: Result<LocallyEncryptedTx> = {
            let mut cipher = self.locally_encrypted_tx_cipher.lock()?;
            let mut rng = McRng;

            Ok(LocallyEncryptedTx(cipher.encrypt_bytes(&mut rng, tx_bytes)))
        };
        let locally_encrypted_tx = maybe_locally_encrypted_tx?;

        let tx_hash = tx.tx_hash();
        let highest_indices = tx.get_membership_proof_highest_indices();
        let key_images: Vec<KeyImage> = tx.key_images();
        let output_public_keys = tx.output_public_keys();

        Ok(TxContext {
            locally_encrypted_tx,
            tx_hash,
            highest_indices,
            key_images,
            output_public_keys,
        })
    }

    fn peer_tx_propose(&self, msg: EnclaveMessage<PeerSession>) -> Result<Vec<TxContext>> {
        // Try and decrypt the message.
        let data = self.ake.peer_decrypt(msg)?;

        // Try and deserialize.
        // Use prost
        let txs = mc_util_serial::decode::<TxList>(&data)?.txs;

        // Convert to TxContexts
        let mut rng = McRng;
        txs.into_iter()
            .map(|tx| {
                let tx_bytes = mc_util_serial::encode(&tx);
                let maybe_locally_encrypted_tx: Result<LocallyEncryptedTx> = {
                    let mut cipher = self.locally_encrypted_tx_cipher.lock()?;
                    Ok(LocallyEncryptedTx(cipher.encrypt_bytes(&mut rng, tx_bytes)))
                };
                let locally_encrypted_tx = maybe_locally_encrypted_tx?;
                let tx_hash = tx.tx_hash();
                let highest_indices = tx.get_membership_proof_highest_indices();
                let key_images: Vec<KeyImage> = tx.key_images();
                let output_public_keys = tx.output_public_keys();

                Ok(TxContext {
                    locally_encrypted_tx,
                    tx_hash,
                    highest_indices,
                    key_images,
                    output_public_keys,
                })
            })
            .collect()
    }

    fn tx_is_well_formed(
        &self,
        locally_encrypted_tx: LocallyEncryptedTx,
        block_index: u64,
        proofs: Vec<TxOutMembershipProof>,
    ) -> Result<(WellFormedEncryptedTx, WellFormedTxContext)> {
        let config = self
            .blockchain_config
            .get()
            .ok_or(Error::NotInitialized)?
            .get_config();

        let ct_min_fee_map = self.ct_min_fee_map.get().ok_or(Error::NotInitialized)?;

        // Enforce that all membership proofs provided by the untrusted system for
        // transaction validation came from the same ledger state. This can be
        // checked by requiring all proofs to have the same root hash.
        let mut root_elements = BTreeSet::new();
        for proof in &proofs {
            let root_element = compute_implied_merkle_root(proof)
                .map_err(|_e| TransactionValidationError::InvalidLedgerContext)?;
            root_elements.insert(root_element);
        }
        if root_elements.len() != 1 {
            return Err(Error::InvalidLocalMembershipProof);
        }

        // Decrypt the locally encrypted transaction.
        let decrypted_bytes = self
            .locally_encrypted_tx_cipher
            .lock()?
            .decrypt_bytes(locally_encrypted_tx.0)?;
        let tx: Tx = mc_util_serial::decode(&decrypted_bytes)?;

        let fee_token_id = TokenId::from(tx.prefix.fee_token_id);

        // Validate.
        let mut csprng = McRng;
        let minimum_fee = ct_min_fee_map
            .get(&fee_token_id)
            .ok_or(TransactionValidationError::TokenNotYetConfigured)?;

        // Make sure any extra token ids that appear in the outputs are also already
        // configured. (this was github issue #1868)
        for token_id in tx.signature.output_token_ids.iter() {
            ct_min_fee_map
                .get(&TokenId::from(token_id))
                .ok_or(TransactionValidationError::TokenNotYetConfigured)?;
        }

        mc_transaction_core::validation::validate(
            &tx,
            block_index,
            config.block_version,
            &proofs,
            minimum_fee,
            &mut csprng,
        )?;

        // Convert into a well formed encrypted transaction + context.
        let well_formed_tx_context = self.get_well_formed_tx_context(&tx, minimum_fee);
        let well_formed_tx = WellFormedTx::from(tx);
        let well_formed_encrypted_tx = self.encrypt_well_formed_tx(&well_formed_tx, &mut csprng)?;

        Ok((well_formed_encrypted_tx, well_formed_tx_context))
    }

    fn txs_for_peer(
        &self,
        encrypted_txs: &[WellFormedEncryptedTx],
        aad: &[u8],
        peer: &PeerSession,
    ) -> Result<EnclaveMessage<PeerSession>> {
        // Quick check that we are aware of this peer. While it might still go away
        // after this check, this allows us to quickly bail out and skip
        // expensive work if the peer is definitely not known to us. This also
        // lets us figure whether we are referencing an incoming or outgoing
        // connection
        if !self.ake.is_peer_known(peer)? {
            return Err(Error::Attest(AttestEnclaveError::NotFound));
        }

        // Decrypt transactions
        let txs: Result<Vec<Tx>> =
            encrypted_txs
                .iter()
                .try_fold(Vec::new(), |mut init, encrypted_tx| {
                    let well_formed_tx = self.decrypt_well_formed_tx(encrypted_tx)?;
                    init.push(well_formed_tx.tx().clone());
                    Ok(init)
                });

        // Serialize this for the peer.
        let serialized_txs = mc_util_serial::encode(&TxList { txs: txs? });

        // Encrypt for the peer.
        Ok(self.ake.peer_encrypt(peer, aad, &serialized_txs)?)
    }

    fn form_block(
        &self,
        parent_block: &Block,
        inputs: FormBlockInputs,
        root_element: &TxOutMembershipElement,
    ) -> Result<(Block, BlockContents, BlockSignature)> {
        let mut rng = McRng;
        let config = self
            .blockchain_config
            .get()
            .ok_or(Error::NotInitialized)?
            .get_config();

        let ct_min_fee_map = self.ct_min_fee_map.get().ok_or(Error::NotInitialized)?;

        if parent_block.version > *config.block_version {
            return Err(Error::BlockVersion(format!("Block version cannot decrease: parent_block.version = {}, config.block_version = {}", parent_block.version, config.block_version)));
        }

        // Get any "original" Tx transactions included in the inputs.
        let transactions = self.get_txs_from_inputs(
            &inputs.well_formed_encrypted_txs_with_proofs,
            parent_block,
            root_element,
            config,
            ct_min_fee_map,
            &mut rng,
        )?;

        // Compute the fee recipient public address
        let fee_public_key = self.get_fee_recipient().map_err(|e| {
            Error::FeePublicAddress(format!("Could not get fee public address: {e:?}"))
        })?;
        let fee_recipient = PublicAddress::new(
            &fee_public_key.spend_public_key,
            &fee_public_key.view_public_key,
        );

        // Compute the total fees for each known token id, for tx's in this block.
        let mut total_fees: CtTokenMap<u128> = ct_min_fee_map.keys().cloned().collect();
        for tx in transactions.iter() {
            let fee_token_id = TokenId::from(tx.prefix.fee_token_id);
            total_fees.add(&fee_token_id, tx.prefix.fee as u128);
        }

        // Sort the token ids which did not appear to the end.
        total_fees.sort_zeroes_to_end();

        // Make fee outputs.
        //
        // We always make a fee output for any fee amount which is nonzero.
        // However, we sometimes make zero value fee outputs, for privacy.
        // The number of fee outputs we make is min(num_tokens, num_transactions).
        //
        // That is, if there is only transaction output in the block, then we only make
        // one fee output. If there are two transactions in the block, we have
        // to make two fee outputs, even if they used the same token, to avoid
        // revealing to these parties that the other person used the same token
        // id as them (active attack). So in that case we will mint a zero value
        // fee.
        //
        // However, we never have to make more fee outputs than there are tokens,
        // no matter how many transactions there are in the block.
        //
        // This strategy is optimal both for blocks with one transaction, and blocks
        // with many transactions -- it only creates some extra fee outputs in the
        // mid-range, which seems okay.
        //
        // Note: The "take" here only makes sense because sort_zeros_to_end was called.
        // This ensures that everything we are ignoring via take has zero value.
        let fee_outputs = total_fees
            .iter()
            .take(min(total_fees.len(), transactions.len()))
            .flat_map(|(token_id, total_fee)| {
                // While unlikely, it is technically possible for the accumulated
                // fee value to overflow a u64, particularly for custom tokens.
                // Therefore, the accumulated fees are stored in a u128.
                // Here we deal with the overflow
                let mut outputs = Vec::default();

                let token_id = *token_id;
                let mut total_fee = *total_fee;

                // Note: This should not be while total_fee > 0, because
                // we want to mint at least one output even when the fee is zero,
                // to avoid revealing that fewer than the number of taken items
                // were nonzero. We are using a "do-while" pattern instead.
                loop {
                    let output_fee = min(total_fee, u64::MAX as u128) as u64;
                    outputs.push(mint_output(
                        config.block_version,
                        &fee_recipient,
                        FEES_OUTPUT_PRIVATE_KEY_DOMAIN_TAG.as_bytes(),
                        parent_block,
                        &transactions,
                        Amount {
                            value: output_fee,
                            token_id,
                        },
                        None,
                        outputs.len(),
                    ));
                    total_fee -= output_fee as u128;
                    if total_fee == 0 {
                        break;
                    }
                }

                outputs
            })
            .collect::<Result<Vec<TxOut>>>()?;

        // Get the list of MintTxs included in the block.
        let mint_txs =
            Self::validate_mint_txs(inputs.mint_txs_with_config, parent_block.index + 1, config)?;

        // Collect outputs and key images.
        let mut outputs: Vec<TxOut> = Vec::new();
        let mut key_images: Vec<KeyImage> = Vec::new();
        for tx in &transactions {
            outputs.extend(tx.prefix.outputs.iter().cloned());
            key_images.extend(tx.key_images().iter().cloned());
        }
        outputs.extend(fee_outputs);

        // Perform minting.
        for (counter, mint_tx) in mint_txs.iter().enumerate() {
            // One last chance to prevent minting MOB.
            if mint_tx.prefix.token_id == Mob::ID {
                return Err(Error::FormBlock("Attempted to mint MOB".into()));
            }

            let recipient = PublicAddress::new(
                &mint_tx.prefix.spend_public_key,
                &mint_tx.prefix.view_public_key,
            );
            let output = mint_output(
                config.block_version,
                &recipient,
                MINTED_OUTPUT_PRIVATE_KEY_DOMAIN_TAG.as_bytes(),
                parent_block,
                &mint_txs,
                Amount {
                    value: mint_tx.prefix.amount,
                    token_id: TokenId::from(mint_tx.prefix.token_id),
                },
                mint_tx.prefix.e_fog_hint.clone(),
                counter,
            )?;

            outputs.push(output);
        }

        // Sort outputs and key images. This removes ordering information which could be
        // used to infer the per-transaction relationships among outputs and/or
        // key images.
        outputs.sort_by(|a, b| a.public_key.cmp(&b.public_key));
        key_images.sort();

        // Get the list of MintConfigTxs included in the block.
        let validated_mint_config_txs = Self::validate_mint_config_txs(
            inputs.mint_config_txs,
            Some(parent_block.index + 1),
            config,
        )?;

        // We purposefully do not ..Default::default() here so that new block fields
        // show up as a compilation error until addressed.
        let block_contents = BlockContents {
            key_images,
            outputs,
            validated_mint_config_txs,
            mint_txs,
        };
        //
        // Form the block.
        let block = Block::new_with_parent(
            config.block_version,
            parent_block,
            root_element,
            &block_contents,
        );

        // Sign the block.
        let public_key = self.ake.get_identity().signing_keypair.lock()?;
        let signature = BlockSignature::from_block_and_keypair(&block, &public_key)?;

        Ok((block, block_contents, signature))
    }
}

/// Creates a single output belonging to a specific recipient account.
/// The output is created using a predictable private key that is derived from
/// the input parameters.
///
/// # Arguments:
/// * `block_version` - The current block version rules for outputs
/// * `recipient` - The recipient of the output.
/// * `domain_tag` - Domain separator for hashing the input parameters.
/// * `parent_block` - The parent block.
/// * `transactions` - The transactions that are included in the current block.
/// * `amount` - Output amount.
/// * `e_fog_hint` - Optional encrypted fog hint to use
/// * `counter` - An additional counter used to disambiguate hashes, when the
///   same amount is minted repeatedly
///
/// Note: The counter value exists to resolve certain corner cases:
/// * If a very large amount > 2 * u64::MAX of some fee is collected, then we
///   would mint at least two fee outputs of value u64::MAX both to the
///   foundation. This hash would be the same for both of these outputs, and so
///   they would have the same private key, and the same public key, and the
///   network would get stuck.
/// * If a governor requests to mint twice in one block, for the same amount and
///   to the same recipient, then this hash would be the same and the network
///   would get stuck, for the same reason.
///
/// The counter value should be used for each "context" where we mint multiple
/// outputs to prevent this issue.
#[allow(clippy::too_many_arguments)]
fn mint_output<T: Digestible>(
    block_version: BlockVersion,
    recipient: &PublicAddress,
    domain_tag: &'static [u8],
    parent_block: &Block,
    transactions: &[T],
    amount: Amount,
    e_fog_hint: Option<EncryptedFogHint>,
    counter: usize,
) -> Result<TxOut> {
    // Check if the token id makes sense right now
    if !block_version.masked_token_id_feature_is_supported() && amount.token_id != 0 {
        return Err(Error::BlockVersion(
            "Cannot mint outputs for non-MOB tokens at this block version".to_string(),
        ));
    }

    // Create a determinstic private key based on the block contents.
    let tx_private_key = {
        let mut hash_value = [0u8; 32];
        {
            let mut transcript = MerlinTranscript::new(domain_tag);
            parent_block
                .id
                .append_to_transcript(b"parent_block_id", &mut transcript);
            transactions.append_to_transcript(b"transactions", &mut transcript);
            amount.append_to_transcript(b"amount", &mut transcript);
            counter.append_to_transcript(b"counter", &mut transcript);
            transcript.extract_digest(&mut hash_value);
        };

        // This private key is generated from the hash of all transactions in this
        // block. This ensures that all nodes generate the same fee output
        // transaction.
        RistrettoPrivate::from(Scalar::from_bytes_mod_order(hash_value))
    };

    // Create a single TxOut
    TxOut::new(
        block_version,
        amount,
        recipient,
        &tx_private_key,
        e_fog_hint.unwrap_or_default(),
    )
    .map_err(|e| Error::FormBlock(format!("NewTxError: {e}")))
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::vec;
    use mc_common::{logger::test_with_logger, HashMap, HashSet};
    use mc_consensus_enclave_api::{GovernorsMap, GovernorsSigner};
    use mc_crypto_keys::{Ed25519Private, Ed25519Signature, Signer};
    use mc_crypto_multisig::{MultiSig, SignerSet};
    use mc_ledger_db::{
        test_utils::{add_txos_to_ledger, create_ledger, create_transaction, initialize_ledger},
        Ledger,
    };
    use mc_transaction_core::{
        mint::{constants::NONCE_LENGTH, MintConfigTxPrefix},
        tx::TxOutMembershipHash,
        validation::validate_tx_out,
        FeeMap,
    };
    use mc_transaction_core_test_utils::{
        create_mint_config_tx_and_signers, create_mint_tx_to_recipient, sign_mint_config_tx_prefix,
        sign_mint_tx_prefix, AccountKey,
    };
    use mc_util_from_random::FromRandom;
    use rand_core::SeedableRng;
    use rand_hc::Hc128Rng;

    // The private keys here are only used by tests. They do not need to be
    // specified for main net. The public keys associated with this private keys
    // are the defaults in build.rs
    const FEE_VIEW_PRIVATE_KEY: [u8; 32] = [
        21, 152, 99, 251, 140, 2, 50, 154, 2, 171, 188, 60, 163, 243, 204, 195, 241, 78, 204, 85,
        202, 52, 250, 242, 215, 247, 175, 59, 121, 185, 111, 8,
    ];

    // This private key was generated using the mc-util-seeded-ed25519-key-gen
    // utility with the seed
    // abababababababababababababababababababababababababababababababab
    const MINTING_TRUST_ROOT_PRIVATE_KEY: [u8; 32] = [
        168, 15, 220, 134, 238, 251, 210, 7, 24, 78, 21, 168, 197, 250, 1, 139, 23, 64, 154, 172,
        192, 125, 32, 148, 183, 78, 167, 52, 170, 254, 120, 64,
    ];

    fn sign_governors_map(map: &GovernorsMap) -> Option<Ed25519Signature> {
        let private_key = Ed25519Private::try_from(&MINTING_TRUST_ROOT_PRIVATE_KEY[..]).unwrap();
        Some(
            Ed25519Pair::from(private_key)
                .sign_governors_map(map)
                .unwrap(),
        )
    }

    #[test_with_logger]
    fn test_enclave_init_validates_blockchain_config(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);
        let (_mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);
        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);
        let governors_map1 =
            GovernorsMap::try_from_iter([(token_id1, signer_set1.clone())]).unwrap();
        let governors_map2 = GovernorsMap::try_from_iter([(token_id2, signer_set1)]).unwrap();
        let enclave = SgxConsensusEnclave::new(logger);
        let block_version = BlockVersion::MAX;

        // Can't initialize without a valid governors signature if we are passing a
        // governors map. This proves to us that BlockchainConfig::validate is being
        // called.
        let blockchain_config = BlockchainConfig {
            block_version,
            governors_map: governors_map1.clone(),
            ..Default::default()
        };
        assert_eq!(
            enclave.enclave_init(
                &Default::default(),
                &Default::default(),
                &None,
                blockchain_config,
            ),
            Err(Error::MissingGovernorsSignature)
        );

        // Can't initialize when the signature does not match the map.
        let blockchain_config = BlockchainConfig {
            block_version,
            governors_map: governors_map1,
            governors_signature: sign_governors_map(&governors_map2),
            ..Default::default()
        };
        assert_eq!(
            enclave.enclave_init(
                &Default::default(),
                &Default::default(),
                &None,
                blockchain_config,
            ),
            Err(Error::InvalidGovernorsSignature)
        );
    }

    #[test_with_logger]
    fn test_tx_is_well_formed_works(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([1u8; 32]);

        for block_version in BlockVersion::iterator() {
            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Create a valid test transaction.
            let sender = AccountKey::random(&mut rng);
            let recipient = AccountKey::random(&mut rng);

            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Choose a TxOut to spend. Only the TxOut in the last block is unspent.
            let block_contents = ledger.get_block_contents(n_blocks - 1).unwrap();
            let tx_out = block_contents.outputs[0].clone();

            let tx = create_transaction(
                block_version,
                &ledger,
                &tx_out,
                &sender,
                &recipient.default_subaddress(),
                n_blocks + 1,
                &mut rng,
            );

            // Create a LocallyEncryptedTx that can be fed into `tx_is_well_formed`.
            let tx_bytes = mc_util_serial::encode(&tx);
            let locally_encrypted_tx = LocallyEncryptedTx(
                enclave
                    .locally_encrypted_tx_cipher
                    .lock()
                    .unwrap()
                    .encrypt_bytes(&mut rng, tx_bytes.clone()),
            );

            // Call `tx_is_well_formed`.
            let highest_indices = tx.get_membership_proof_highest_indices();
            let proofs = ledger
                .get_tx_out_proof_of_memberships(&highest_indices)
                .expect("failed getting proofs");
            let block_index = ledger.num_blocks().unwrap();
            let (well_formed_encrypted_tx, well_formed_tx_context) = enclave
                .tx_is_well_formed(locally_encrypted_tx.clone(), block_index, proofs)
                .unwrap();

            // Check that the context we got back is correct.
            const SMALLEST_MINIMUM_FEE: u64 = 1 << SMALLEST_MINIMUM_FEE_LOG2;
            let (expected_priority, _) =
                ct_u64_divide(tx.prefix.fee, Mob::MINIMUM_FEE / SMALLEST_MINIMUM_FEE);

            assert_eq!(well_formed_tx_context.tx_hash(), &tx.tx_hash());
            assert_eq!(well_formed_tx_context.priority(), expected_priority);
            assert_eq!(
                well_formed_tx_context.tombstone_block(),
                tx.prefix.tombstone_block
            );
            assert_eq!(*well_formed_tx_context.key_images(), tx.key_images());

            // All three tx representations should be different.
            assert_ne!(tx_bytes, locally_encrypted_tx.0);
            assert_ne!(tx_bytes, well_formed_encrypted_tx.0);
            assert_ne!(locally_encrypted_tx.0, well_formed_encrypted_tx.0);

            // Check that we can go back from the encrypted tx to the original tx.
            let well_formed_tx = enclave
                .decrypt_well_formed_tx(&well_formed_encrypted_tx)
                .unwrap();
            assert_eq!(tx, well_formed_tx.tx);
        }
    }

    #[test_with_logger]
    fn test_tx_is_well_formed_works_errors_on_bad_inputs(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        for block_version in BlockVersion::iterator() {
            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Create a valid test transaction.
            let sender = AccountKey::random(&mut rng);
            let recipient = AccountKey::random(&mut rng);

            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Choose a TxOut to spend. Only the TxOut in the last block is unspent.
            let block_contents = ledger.get_block_contents(n_blocks - 1).unwrap();
            let tx_out = block_contents.outputs[0].clone();

            let tx = create_transaction(
                block_version,
                &ledger,
                &tx_out,
                &sender,
                &recipient.default_subaddress(),
                n_blocks + 1,
                &mut rng,
            );

            // Create a LocallyEncryptedTx that can be fed into `tx_is_well_formed`.
            let tx_bytes = mc_util_serial::encode(&tx);
            let locally_encrypted_tx = LocallyEncryptedTx(
                enclave
                    .locally_encrypted_tx_cipher
                    .lock()
                    .unwrap()
                    .encrypt_bytes(&mut rng, tx_bytes.clone()),
            );

            // Call `tx_is_well_formed` with a block index that puts us past the tombstone
            // block.
            let highest_indices = tx.get_membership_proof_highest_indices();
            let proofs = ledger
                .get_tx_out_proof_of_memberships(&highest_indices)
                .expect("failed getting proofs");
            let block_index = ledger.num_blocks().unwrap();

            assert_eq!(
                enclave.tx_is_well_formed(
                    locally_encrypted_tx.clone(),
                    block_index + mc_transaction_core::constants::MAX_TOMBSTONE_BLOCKS,
                    proofs.clone(),
                ),
                Err(Error::MalformedTx(
                    TransactionValidationError::TombstoneBlockExceeded
                ))
            );

            // Call `tx_is_well_formed` with a wrong proof.
            let mut bad_proofs = proofs.clone();
            bad_proofs[0].elements[0].hash = TxOutMembershipHash::from([123; 32]);

            assert_eq!(
                enclave.tx_is_well_formed(locally_encrypted_tx.clone(), block_index, bad_proofs,),
                Err(Error::InvalidLocalMembershipProof)
            );

            // Corrupt the encrypted data.
            let mut corrupted_locally_encrypted_tx = locally_encrypted_tx.clone();
            corrupted_locally_encrypted_tx.0[0] = !corrupted_locally_encrypted_tx.0[0];

            assert_eq!(
                enclave.tx_is_well_formed(corrupted_locally_encrypted_tx, block_index, proofs),
                Err(Error::CacheCipher(
                    mc_crypto_message_cipher::CipherError::MacFailure
                ))
            );
        }
    }

    #[test_with_logger]
    // tx_is_well_formed rejects inconsistent root elements.
    fn test_tx_is_well_form_rejects_inconsistent_root_elements(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        for block_version in BlockVersion::iterator() {
            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Construct TxOutMembershipProofs.
            let mut ledger = create_ledger();
            let n_blocks = 16;
            let account_key = AccountKey::random(&mut rng);
            initialize_ledger(block_version, &mut ledger, n_blocks, &account_key, &mut rng);

            let n_proofs = 10;
            let indexes: Vec<u64> = (0..n_proofs as u64).collect();
            let mut membership_proofs = ledger.get_tx_out_proof_of_memberships(&indexes).unwrap();
            // Modify one of the proofs to have a different root hash.
            let inconsistent_proof = &mut membership_proofs[7];
            // TODO: check this
            let root_element = inconsistent_proof.elements.last_mut().unwrap();
            root_element.hash = TxOutMembershipHash::from([33u8; 32]);

            // The membership proofs supplied by the server are checked before this is
            // decrypted and validated, so it can just be constructed from an empty
            // vector of bytes.
            let locally_encrypted_tx = LocallyEncryptedTx(Vec::new());
            let block_index = 77;
            let result =
                enclave.tx_is_well_formed(locally_encrypted_tx, block_index, membership_proofs);
            let expected = Err(Error::InvalidLocalMembershipProof);
            assert_eq!(result, expected);
        }
    }

    #[test_with_logger]
    fn test_form_block_works_single_token(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        for block_version in BlockVersion::iterator() {
            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Create a valid test transaction.
            let sender = AccountKey::random(&mut rng);
            let recipient = AccountKey::random(&mut rng);

            let mut ledger = create_ledger();
            let n_blocks = 1;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Spend outputs from the origin block.
            let origin_block_contents = ledger.get_block_contents(0).unwrap();

            let input_transactions: Vec<Tx> = (0..3)
                .map(|i| {
                    let tx_out = origin_block_contents.outputs[i].clone();

                    create_transaction(
                        block_version,
                        &ledger,
                        &tx_out,
                        &sender,
                        &recipient.default_subaddress(),
                        n_blocks + 1,
                        &mut rng,
                    )
                })
                .collect();

            let total_fee: u64 = input_transactions.iter().map(|tx| tx.prefix.fee).sum();

            // Create WellFormedEncryptedTxs + proofs
            let well_formed_encrypted_txs_with_proofs: Vec<_> = input_transactions
                .iter()
                .map(|tx| {
                    let well_formed_tx = WellFormedTx::from(tx.clone());
                    let encrypted_tx = enclave
                        .encrypt_well_formed_tx(&well_formed_tx, &mut rng)
                        .unwrap();

                    let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices();
                    let membership_proofs = ledger
                        .get_tx_out_proof_of_memberships(&highest_indices)
                        .expect("failed getting proof");
                    (encrypted_tx, membership_proofs)
                })
                .collect();

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();
            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let (block, block_contents, signature) = enclave
                .form_block(
                    &parent_block,
                    FormBlockInputs {
                        well_formed_encrypted_txs_with_proofs,
                        ..Default::default()
                    },
                    &root_element,
                )
                .unwrap();

            // Verify signature.
            {
                assert_eq!(
                    signature.signer(),
                    &enclave
                        .ake
                        .get_identity()
                        .signing_keypair
                        .lock()
                        .unwrap()
                        .public_key()
                );

                assert!(signature.verify(&block).is_ok());
            }

            // `block_contents` should include the aggregate fee.

            let num_outputs: usize = input_transactions
                .iter()
                .map(|tx| tx.prefix.outputs.len())
                .sum();
            assert_eq!(num_outputs + 1, block_contents.outputs.len());

            // One of the outputs should be the aggregate fee.
            let fee_view_key = RistrettoPrivate::try_from(&FEE_VIEW_PRIVATE_KEY).unwrap();

            let fee_output = block_contents
                .outputs
                .iter()
                .find(|output| output.view_key_match(&fee_view_key).is_ok())
                .unwrap();

            // The value of the aggregate fee should equal the total value of fees in the
            // input transaction.
            let (amount, _) = fee_output.view_key_match(&fee_view_key).unwrap();
            assert_eq!(amount.value, total_fee);
            assert_eq!(amount.token_id, Mob::ID);

            // The outputs should all conform to block version rules
            for output in block_contents.outputs.iter() {
                validate_tx_out(block_version, output).unwrap();
            }
        }
    }

    #[test_with_logger]
    fn test_form_block_works_multiple_tokens(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);

        for block_version in BlockVersion::iterator() {
            if !block_version.masked_token_id_feature_is_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                fee_map: FeeMap::try_from_iter([
                    (Mob::ID, 2_000_000),
                    (token_id1, 1024),
                    (token_id2, 1024),
                ])
                .unwrap(),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Create a valid test transaction.
            let sender = AccountKey::random(&mut rng);
            let recipient = AccountKey::random(&mut rng);

            let mut ledger = create_ledger();
            let n_blocks = 2;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Spend outputs from the origin block. These are token_id=0.
            let origin_block_contents = ledger.get_block_contents(0).unwrap();

            let mut input_transactions: Vec<Tx> = (0..3)
                .map(|i| {
                    let tx_out = origin_block_contents.outputs[i].clone();

                    create_transaction(
                        block_version,
                        &ledger,
                        &tx_out,
                        &sender,
                        &recipient.default_subaddress(),
                        n_blocks + 20,
                        &mut rng,
                    )
                })
                .collect();

            // Generate transactions for the non-MOB token ids.
            for token_id in [token_id1, token_id2] {
                for _ in 0..5 {
                    let spendable_output = TxOut::new(
                        block_version,
                        Amount::new(10_000_000_000, token_id),
                        &sender.default_subaddress(),
                        &RistrettoPrivate::from_random(&mut rng),
                        Default::default(),
                    )
                    .unwrap();

                    add_txos_to_ledger(
                        &mut ledger,
                        block_version,
                        &[spendable_output.clone()],
                        &mut rng,
                    )
                    .unwrap();

                    let tx = create_transaction(
                        block_version,
                        &ledger,
                        &spendable_output,
                        &sender,
                        &recipient.default_subaddress(),
                        n_blocks + 20,
                        &mut rng,
                    );

                    input_transactions.push(tx);
                }
            }

            // Remove the last token_id2 transaction - this is so that we have a different
            // number of transactions for each token id. This makes the
            // aggregated fee unique per token id - see comment down below on fee
            // verification.
            input_transactions.pop();

            // Create WellFormedEncryptedTxs + proofs
            let well_formed_encrypted_txs_with_proofs: Vec<_> = input_transactions
                .iter()
                .map(|tx| {
                    let well_formed_tx = WellFormedTx::from(tx.clone());
                    let encrypted_tx = enclave
                        .encrypt_well_formed_tx(&well_formed_tx, &mut rng)
                        .unwrap();

                    let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices();
                    let membership_proofs = ledger
                        .get_tx_out_proof_of_memberships(&highest_indices)
                        .expect("failed getting proof");
                    (encrypted_tx, membership_proofs)
                })
                .collect();

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();
            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let (block, block_contents, signature) = enclave
                .form_block(
                    &parent_block,
                    FormBlockInputs {
                        well_formed_encrypted_txs_with_proofs,
                        ..Default::default()
                    },
                    &root_element,
                )
                .unwrap();

            // Verify signature.
            {
                assert_eq!(
                    signature.signer(),
                    &enclave
                        .ake
                        .get_identity()
                        .signing_keypair
                        .lock()
                        .unwrap()
                        .public_key()
                );

                assert!(signature.verify(&block).is_ok());
            }

            // `block_contents` should include three aggregate fee outputs.
            let num_outputs: usize = input_transactions
                .iter()
                .map(|tx| tx.prefix.outputs.len())
                .sum();
            assert_eq!(num_outputs + 3, block_contents.outputs.len());

            let fee_view_key = RistrettoPrivate::try_from(&FEE_VIEW_PRIVATE_KEY).unwrap();

            let fee_amounts = block_contents
                .outputs
                .iter()
                .filter_map(|output| match output.view_key_match(&fee_view_key) {
                    Ok((amount, _)) => Some(amount),
                    Err(_) => None,
                })
                .collect::<Vec<_>>();
            assert_eq!(fee_amounts.len(), 3);

            // See that we got the expected amounts.
            // The fee amount created by the test code is Mob::MINIMUM_FEE, however the
            // number of transactions we create varries based on the token id.
            // We create 3 mob txs, 5 token_id1 txs and 4 token_id2 txs.
            let expected_fee_amounts = HashMap::from_iter([
                (Mob::ID, Mob::MINIMUM_FEE * 3),
                (token_id1, Mob::MINIMUM_FEE * 5),
                (token_id2, Mob::MINIMUM_FEE * 4),
            ]);

            let resulted_fee_amounts = HashMap::from_iter(
                fee_amounts
                    .iter()
                    .map(|amount| (amount.token_id, amount.value)),
            );

            assert_eq!(expected_fee_amounts, resulted_fee_amounts);

            // The outputs should all conform to block version rules
            for output in block_contents.outputs.iter() {
                validate_tx_out(block_version, output).unwrap();
            }

            // All output keys should be unique.
            let target_keys = HashSet::from_iter(
                block_contents
                    .outputs
                    .iter()
                    .map(|output| output.target_key),
            );
            assert_eq!(target_keys.len(), block_contents.outputs.len());

            let public_keys = HashSet::from_iter(
                block_contents
                    .outputs
                    .iter()
                    .map(|output| output.public_key),
            );
            assert_eq!(public_keys.len(), block_contents.outputs.len());
        }
    }

    #[test_with_logger]
    /// form_block should return an error if the input transactions contain a
    /// double-spend.
    fn test_form_block_prevents_duplicate_spend(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        for block_version in BlockVersion::iterator() {
            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger. `sender` is the owner of all outputs in the initial
            // ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Create a few transactions from `sender` to `recipient`.
            let num_transactions = 5;
            let recipient = AccountKey::random(&mut rng);

            // The first block contains RING_SIZE outputs.
            let block_zero_contents = ledger.get_block_contents(0).unwrap();

            let mut new_transactions = Vec::new();
            for i in 0..num_transactions {
                let tx_out = &block_zero_contents.outputs[i];

                let tx = create_transaction(
                    block_version,
                    &ledger,
                    tx_out,
                    &sender,
                    &recipient.default_subaddress(),
                    n_blocks + 1,
                    &mut rng,
                );
                new_transactions.push(tx);
            }

            // Create another transaction that spends the zero^th output in block zero.
            let double_spend = {
                let tx_out = &block_zero_contents.outputs[0];

                create_transaction(
                    block_version,
                    &ledger,
                    tx_out,
                    &sender,
                    &recipient.default_subaddress(),
                    n_blocks + 1,
                    &mut rng,
                )
            };
            new_transactions.push(double_spend);

            // Create WellFormedEncryptedTxs + proofs
            let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions
                .iter()
                .map(|tx| {
                    let well_formed_tx = WellFormedTx::from(tx.clone());
                    let encrypted_tx = enclave
                        .encrypt_well_formed_tx(&well_formed_tx, &mut rng)
                        .unwrap();

                    let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices();
                    let membership_proofs = ledger
                        .get_tx_out_proof_of_memberships(&highest_indices)
                        .expect("failed getting proof");
                    (encrypted_tx, membership_proofs)
                })
                .collect();

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();
            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    well_formed_encrypted_txs_with_proofs,
                    ..Default::default()
                },
                &root_element,
            );
            let expected_duplicate_key_image = new_transactions[0].key_images()[0];

            // Check
            let expected = Err(Error::FormBlock(format!(
                "Duplicate key image: {expected_duplicate_key_image:?}"
            )));

            assert_eq!(form_block_result, expected);
        }
    }

    #[test_with_logger]
    /// form_block should return an error if the input transactions contain a
    /// duplicate output public key.
    fn test_form_block_prevents_duplicate_output_public_key(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        for block_version in BlockVersion::iterator() {
            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger. `sender` is the owner of all outputs in the initial
            // ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Create a few transactions from `sender` to `recipient`.
            let num_transactions = 5;
            let recipient = AccountKey::random(&mut rng);

            // The first block contains RING_SIZE outputs.
            let block_zero_contents = ledger.get_block_contents(0).unwrap();

            // Re-create the rng so that we could more easily generate a duplicate output
            // public key.
            let mut rng = Hc128Rng::from_seed([77u8; 32]);

            let mut new_transactions = Vec::new();
            for i in 0..num_transactions - 1 {
                let tx_out = &block_zero_contents.outputs[i];

                let tx = create_transaction(
                    block_version,
                    &ledger,
                    tx_out,
                    &sender,
                    &recipient.default_subaddress(),
                    n_blocks + 1,
                    &mut rng,
                );
                new_transactions.push(tx);
            }

            // Re-creating the rng here would result in a duplicate output public key.
            {
                let mut rng = Hc128Rng::from_seed([77u8; 32]);
                let tx_out = &block_zero_contents.outputs[num_transactions - 1];

                let tx = create_transaction(
                    block_version,
                    &ledger,
                    tx_out,
                    &sender,
                    &recipient.default_subaddress(),
                    n_blocks + 1,
                    &mut rng,
                );
                new_transactions.push(tx);

                assert_eq!(
                    new_transactions[0].prefix.outputs[0].public_key,
                    new_transactions[num_transactions - 1].prefix.outputs[0].public_key,
                );
            }

            // Create WellFormedEncryptedTxs + proofs
            let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions
                .iter()
                .map(|tx| {
                    let well_formed_tx = WellFormedTx::from(tx.clone());
                    let encrypted_tx = enclave
                        .encrypt_well_formed_tx(&well_formed_tx, &mut rng)
                        .unwrap();

                    let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices();
                    let membership_proofs = ledger
                        .get_tx_out_proof_of_memberships(&highest_indices)
                        .expect("failed getting proof");
                    (encrypted_tx, membership_proofs)
                })
                .collect();

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();
            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    well_formed_encrypted_txs_with_proofs,
                    ..Default::default()
                },
                &root_element,
            );
            let expected_duplicate_output_public_key = new_transactions[0].output_public_keys()[0];

            // Check
            let expected = Err(Error::FormBlock(format!(
                "Duplicate output public key: {expected_duplicate_output_public_key:?}"
            )));

            assert_eq!(form_block_result, expected);
        }
    }

    #[test_with_logger]
    fn form_block_refuses_duplicate_root_elements(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        for block_version in BlockVersion::iterator() {
            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger. `sender` is the owner of all outputs in the initial
            // ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            let mut ledger2 = create_ledger();
            initialize_ledger(block_version, &mut ledger2, n_blocks + 1, &sender, &mut rng);

            // Create a few transactions from `sender` to `recipient`.
            let num_transactions = 6;
            let recipient = AccountKey::random(&mut rng);

            // The first block contains a single transaction with RING_SIZE outputs.
            let block_zero_contents = ledger.get_block_contents(0).unwrap();

            let mut new_transactions = Vec::new();
            for i in 0..num_transactions {
                let tx_out = &block_zero_contents.outputs[i];

                let tx = create_transaction(
                    block_version,
                    &ledger,
                    tx_out,
                    &sender,
                    &recipient.default_subaddress(),
                    n_blocks + 1,
                    &mut rng,
                );
                new_transactions.push(tx);
            }

            // Create WellFormedEncryptedTxs + proofs
            let well_formed_encrypted_txs_with_proofs: Vec<(
                WellFormedEncryptedTx,
                Vec<TxOutMembershipProof>,
            )> = new_transactions
                .iter()
                .enumerate()
                .map(|(tx_idx, tx)| {
                    let well_formed_tx = WellFormedTx::from(tx.clone());
                    let encrypted_tx = enclave
                        .encrypt_well_formed_tx(&well_formed_tx, &mut rng)
                        .unwrap();

                    let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices();
                    let membership_proofs = highest_indices
                        .iter()
                        .map(|index| {
                            // Make one of the proofs have a different root element by creating it
                            // from a different
                            if tx_idx == 0 {
                                ledger2
                                    .get_tx_out_proof_of_memberships(&[*index])
                                    .expect("failed getting proof")[0]
                                    .clone()
                            } else {
                                ledger
                                    .get_tx_out_proof_of_memberships(&[*index])
                                    .expect("failed getting proof")[0]
                                    .clone()
                            }
                        })
                        .collect();
                    (encrypted_tx, membership_proofs)
                })
                .collect();

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();
            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    well_formed_encrypted_txs_with_proofs,
                    ..Default::default()
                },
                &root_element,
            );

            // Check
            let expected = Err(Error::MalformedTx(
                TransactionValidationError::InvalidTxOutMembershipProof,
            ));
            assert_eq!(form_block_result, expected);
        }
    }

    #[test_with_logger]
    fn form_block_refuses_incorrect_root_element(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        for block_version in BlockVersion::iterator() {
            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger. `sender` is the owner of all outputs in the initial
            // ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Create a few transactions from `sender` to `recipient`.
            let num_transactions = 6;
            let recipient = AccountKey::random(&mut rng);

            // The first block contains a single transaction with RING_SIZE outputs.
            let block_zero_contents = ledger.get_block_contents(0).unwrap();

            let mut new_transactions = Vec::new();
            for i in 0..num_transactions {
                let tx_out = &block_zero_contents.outputs[i];

                let tx = create_transaction(
                    block_version,
                    &ledger,
                    tx_out,
                    &sender,
                    &recipient.default_subaddress(),
                    n_blocks + 1,
                    &mut rng,
                );
                new_transactions.push(tx);
            }

            // Create WellFormedEncryptedTxs + proofs
            let well_formed_encrypted_txs_with_proofs: Vec<_> = new_transactions
                .iter()
                .map(|tx| {
                    let well_formed_tx = WellFormedTx::from(tx.clone());
                    let encrypted_tx = enclave
                        .encrypt_well_formed_tx(&well_formed_tx, &mut rng)
                        .unwrap();

                    let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices();
                    let membership_proofs = ledger
                        .get_tx_out_proof_of_memberships(&highest_indices)
                        .expect("failed getting proof");
                    (encrypted_tx, membership_proofs)
                })
                .collect();

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();
            let mut root_element = ledger.get_root_tx_out_membership_element().unwrap();

            // Alter the root element so that it is inconsistent with the proofs.
            root_element.hash.0[0] = !root_element.hash.0[0];

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    well_formed_encrypted_txs_with_proofs,
                    ..Default::default()
                },
                &root_element,
            );

            // Check
            let expected = Err(Error::InvalidLocalMembershipRootElement);
            assert_eq!(form_block_result, expected);
        }
    }

    #[test_with_logger]
    fn form_block_refuses_decreasing_block_version(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        for block_version in BlockVersion::iterator().skip(1) {
            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version: BlockVersion::try_from(*block_version - 1).unwrap(),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger. `sender` is the owner of all outputs in the initial
            // ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Create a few transactions from `sender` to `recipient`.
            let num_transactions = 6;
            let recipient = AccountKey::random(&mut rng);

            // The first block contains a single transaction with RING_SIZE outputs.
            let block_zero_contents = ledger.get_block_contents(0).unwrap();

            let mut new_transactions = Vec::new();
            for i in 0..num_transactions {
                let tx_out = &block_zero_contents.outputs[i];

                let tx = create_transaction(
                    block_version,
                    &ledger,
                    tx_out,
                    &sender,
                    &recipient.default_subaddress(),
                    n_blocks + 1,
                    &mut rng,
                );
                new_transactions.push(tx);
            }

            // Create WellFormedEncryptedTxs + proofs
            let well_formed_encrypted_txs_with_proofs: Vec<(
                WellFormedEncryptedTx,
                Vec<TxOutMembershipProof>,
            )> = new_transactions
                .iter()
                .map(|tx| {
                    let well_formed_tx = WellFormedTx::from(tx.clone());
                    let encrypted_tx = enclave
                        .encrypt_well_formed_tx(&well_formed_tx, &mut rng)
                        .unwrap();

                    let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices();
                    let membership_proofs = ledger
                        .get_tx_out_proof_of_memberships(&highest_indices)
                        .expect("failed getting proof");
                    (encrypted_tx, membership_proofs)
                })
                .collect();

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    well_formed_encrypted_txs_with_proofs,
                    ..Default::default()
                },
                &root_element,
            );

            log::info!(logger, "got form block result: {:?}", form_block_result);

            // Check if we get a form block error as expected
            match form_block_result {
                Err(Error::BlockVersion(_)) => {}
                _ => panic!(
                    "Expected a BlockVersion error due to config.block_version being less than parent"
                ),
            }
        }
    }

    #[test_with_logger]
    fn form_block_can_mint_new_tokens(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);

        let (mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);
        let (mint_config_tx2, signers2) = create_mint_config_tx_and_signers(token_id2, &mut rng);

        let recipient1 = AccountKey::random(&mut rng);
        let recipient2 = AccountKey::random(&mut rng);

        let mint_tx1 = create_mint_tx_to_recipient(
            token_id1,
            &signers1,
            12,
            &recipient1.default_subaddress(),
            &mut rng,
        );
        let mint_tx2 = create_mint_tx_to_recipient(
            token_id2,
            &signers2,
            200,
            &recipient2.default_subaddress(),
            &mut rng,
        );

        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);
        let signer_set2 = SignerSet::new(signers2.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map =
            GovernorsMap::try_from_iter([(token_id1, signer_set1), (token_id2, signer_set2)])
                .unwrap();

        for block_version in BlockVersion::iterator() {
            if !block_version.mint_transactions_are_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                governors_map: governors_map.clone(),
                governors_signature: sign_governors_map(&governors_map),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();

            // We want the next block that getts appended to the ledger to exceed the
            // tombstone limit of the mint config tx, since we want to make sure
            // that minting that relies on an old MintConfigTx (one that is past
            // its tombstone block) still validate and mint successfully.
            let n_blocks = mint_config_tx1.prefix.tombstone_block;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let (block, block_contents, signature) = enclave
                .form_block(
                    &parent_block,
                    FormBlockInputs {
                        mint_txs_with_config: vec![
                            (
                                mint_tx1.clone(),
                                mint_config_tx1.clone(),
                                mint_config_tx1.prefix.configs[0].clone(),
                            ),
                            (
                                mint_tx2.clone(),
                                mint_config_tx2.clone(),
                                mint_config_tx2.prefix.configs[0].clone(),
                            ),
                        ],
                        ..Default::default()
                    },
                    &root_element,
                )
                .unwrap();

            // Verify signature
            assert_eq!(
                signature.signer(),
                &enclave
                    .ake
                    .get_identity()
                    .signing_keypair
                    .lock()
                    .unwrap()
                    .public_key()
            );

            assert!(signature.verify(&block).is_ok());

            // The block contents should contain the two mint txs.
            assert_eq!(
                block_contents.mint_txs,
                vec![mint_tx1.clone(), mint_tx2.clone()]
            );

            // There should be no key images or mint config txs
            assert!(block_contents.key_images.is_empty());
            assert!(block_contents.validated_mint_config_txs.is_empty());

            // The block contents should contain the minted tx outs.
            assert_eq!(block_contents.outputs.len(), 2);

            let output1 = block_contents
                .outputs
                .iter()
                .find(|output| output.view_key_match(recipient1.view_private_key()).is_ok())
                .unwrap();
            let (amount, _) = output1
                .view_key_match(recipient1.view_private_key())
                .unwrap();
            assert_eq!(amount.value, 12);
            assert_eq!(amount.token_id, token_id1);

            let output2 = block_contents
                .outputs
                .iter()
                .find(|output| output.view_key_match(recipient2.view_private_key()).is_ok())
                .unwrap();
            let (amount, _) = output2
                .view_key_match(recipient2.view_private_key())
                .unwrap();
            assert_eq!(amount.value, 200);
            assert_eq!(amount.token_id, token_id2);
        }
    }

    #[test_with_logger]
    fn form_block_accepts_valid_nested_multisig_mint(logger: Logger) {
        let token_id1 = TokenId::from(1);
        let mut rng = Hc128Rng::from_seed([77u8; 32]);
        let block_version = BlockVersion::MAX;

        // Ensure the chosen block version supports the feature being tested here.
        assert!(block_version.mint_transactions_are_supported());
        assert!(block_version.nested_multisigs_are_supported());

        let governor_1 = Ed25519Pair::from_random(&mut rng);
        let governor_2 = Ed25519Pair::from_random(&mut rng);
        let governor_3 = Ed25519Pair::from_random(&mut rng);

        let governors_signer_set = SignerSet::new_with_multi(
            vec![governor_1.public_key()],
            vec![SignerSet::new(
                vec![governor_2.public_key(), governor_3.public_key()],
                2,
            )],
            2,
        );
        let governors_map =
            GovernorsMap::try_from_iter([(token_id1, governors_signer_set)]).unwrap();

        let minter_1 = Ed25519Pair::from_random(&mut rng);
        let minter_2 = Ed25519Pair::from_random(&mut rng);
        let minter_3 = Ed25519Pair::from_random(&mut rng);

        let minters_signer_set = SignerSet::new_with_multi(
            vec![minter_1.public_key()],
            vec![SignerSet::new(
                vec![minter_2.public_key(), minter_3.public_key()],
                2,
            )],
            2,
        );

        let mut nonce: Vec<u8> = vec![0u8; NONCE_LENGTH];
        rng.fill_bytes(&mut nonce);

        let mint_config_tx = {
            let prefix = MintConfigTxPrefix {
                token_id: *token_id1,
                configs: vec![MintConfig {
                    token_id: *token_id1,
                    signer_set: minters_signer_set,
                    mint_limit: 10000,
                }],
                nonce,
                tombstone_block: 2,
                total_mint_limit: 10000,
            };
            let message = prefix.hash();
            let signature = MultiSig::new(vec![
                governor_1.sign(message.as_ref()),
                governor_2.sign(message.as_ref()),
                governor_3.sign(message.as_ref()),
            ]);
            MintConfigTx { prefix, signature }
        };

        let recipient1 = AccountKey::random(&mut rng);

        let mint_tx = create_mint_tx_to_recipient(
            token_id1,
            &[minter_1, minter_2, minter_3],
            12,
            &recipient1.default_subaddress(),
            &mut rng,
        );

        let enclave = SgxConsensusEnclave::new(logger);
        let blockchain_config = BlockchainConfig {
            block_version,
            governors_map: governors_map.clone(),
            governors_signature: sign_governors_map(&governors_map),
            ..Default::default()
        };
        enclave
            .enclave_init(
                &Default::default(),
                &Default::default(),
                &None,
                blockchain_config,
            )
            .unwrap();

        // Initialize a ledger.
        let sender = AccountKey::random(&mut rng);
        let mut ledger = create_ledger();
        initialize_ledger(block_version, &mut ledger, 1, &sender, &mut rng);

        // Form block
        let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

        let root_element = ledger.get_root_tx_out_membership_element().unwrap();

        let (block, block_contents, signature) = enclave
            .form_block(
                &parent_block,
                FormBlockInputs {
                    mint_txs_with_config: vec![(
                        mint_tx.clone(),
                        mint_config_tx.clone(),
                        mint_config_tx.prefix.configs[0].clone(),
                    )],
                    ..Default::default()
                },
                &root_element,
            )
            .unwrap();

        // Verify signature
        assert_eq!(
            signature.signer(),
            &enclave
                .ake
                .get_identity()
                .signing_keypair
                .lock()
                .unwrap()
                .public_key()
        );

        assert!(signature.verify(&block).is_ok());

        assert_eq!(block_contents.mint_txs, vec![mint_tx]);

        // There should be no key images or mint config txs
        assert!(block_contents.key_images.is_empty());
        assert!(block_contents.validated_mint_config_txs.is_empty());

        // The block contents should contain the minted tx out.
        assert_eq!(block_contents.outputs.len(), 1);

        let output1 = &block_contents.outputs[0];
        let (amount, _) = output1
            .view_key_match(recipient1.view_private_key())
            .unwrap();
        assert_eq!(amount, Amount::new(12, token_id1));
    }

    #[test_with_logger]
    fn form_block_rejects_mints_with_partially_signed_nested_multisigs(logger: Logger) {
        let token_id1 = TokenId::from(1);
        let mut rng = Hc128Rng::from_seed([77u8; 32]);
        let block_version = BlockVersion::MAX;

        // Ensure the chosen block version supports the feature being tested here.
        assert!(block_version.mint_transactions_are_supported());
        assert!(block_version.nested_multisigs_are_supported());

        let governor_1 = Ed25519Pair::from_random(&mut rng);
        let governor_2 = Ed25519Pair::from_random(&mut rng);
        let governor_3 = Ed25519Pair::from_random(&mut rng);

        let governors_signer_set = SignerSet::new_with_multi(
            vec![governor_1.public_key()],
            vec![SignerSet::new(
                vec![governor_2.public_key(), governor_3.public_key()],
                2,
            )],
            2,
        );
        let governors_map =
            GovernorsMap::try_from_iter([(token_id1, governors_signer_set)]).unwrap();

        let minter_1 = Ed25519Pair::from_random(&mut rng);
        let minter_2 = Ed25519Pair::from_random(&mut rng);
        let minter_3 = Ed25519Pair::from_random(&mut rng);

        let minters_signer_set = SignerSet::new_with_multi(
            vec![minter_1.public_key()],
            vec![SignerSet::new(
                vec![minter_2.public_key(), minter_3.public_key()],
                2,
            )],
            2,
        );

        let mut nonce: Vec<u8> = vec![0u8; NONCE_LENGTH];
        rng.fill_bytes(&mut nonce);

        let mint_config_tx_prefix = MintConfigTxPrefix {
            token_id: *token_id1,
            configs: vec![MintConfig {
                token_id: *token_id1,
                signer_set: minters_signer_set,
                mint_limit: 10000,
            }],
            nonce,
            tombstone_block: 2,
            total_mint_limit: 10000,
        };
        let message = mint_config_tx_prefix.hash();

        // Signed with all governors
        let valid_mint_config_tx = {
            let signature = MultiSig::new(vec![
                governor_1.sign(message.as_ref()),
                governor_2.sign(message.as_ref()),
                governor_3.sign(message.as_ref()),
            ]);
            MintConfigTx {
                prefix: mint_config_tx_prefix.clone(),
                signature,
            }
        };

        // Missing one governor signature.
        let invalid_mint_config_tx = {
            let signature = MultiSig::new(vec![
                governor_1.sign(message.as_ref()),
                governor_2.sign(message.as_ref()),
            ]);
            MintConfigTx {
                prefix: mint_config_tx_prefix,
                signature,
            }
        };

        let recipient1 = AccountKey::random(&mut rng);

        // Signed without one of the needed minters
        let invalid_mint_tx = create_mint_tx_to_recipient(
            token_id1,
            &[
                Ed25519Pair::from(minter_1.private_key()),
                Ed25519Pair::from(minter_2.private_key()),
            ],
            12,
            &recipient1.default_subaddress(),
            &mut rng,
        );

        // Signed with all minters
        let valid_mint_tx = create_mint_tx_to_recipient(
            token_id1,
            &[minter_1, minter_2, minter_3],
            12,
            &recipient1.default_subaddress(),
            &mut rng,
        );

        let enclave = SgxConsensusEnclave::new(logger);
        let blockchain_config = BlockchainConfig {
            block_version,
            governors_map: governors_map.clone(),
            governors_signature: sign_governors_map(&governors_map),
            ..Default::default()
        };
        enclave
            .enclave_init(
                &Default::default(),
                &Default::default(),
                &None,
                blockchain_config,
            )
            .unwrap();

        // Initialize a ledger.
        let sender = AccountKey::random(&mut rng);
        let mut ledger = create_ledger();
        initialize_ledger(block_version, &mut ledger, 1, &sender, &mut rng);

        let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();
        let root_element = ledger.get_root_tx_out_membership_element().unwrap();

        // Form block with a valid mint config but invalid mint tx
        let result = enclave.form_block(
            &parent_block,
            FormBlockInputs {
                mint_txs_with_config: vec![(
                    invalid_mint_tx,
                    valid_mint_config_tx.clone(),
                    valid_mint_config_tx.prefix.configs[0].clone(),
                )],
                ..Default::default()
            },
            &root_element,
        );
        assert_eq!(
            result,
            Err(Error::MalformedMintingTx(
                MintValidationError::InvalidSignature
            ))
        );

        // Form block with an invalid mint config but a valid mint tx
        let result = enclave.form_block(
            &parent_block,
            FormBlockInputs {
                mint_txs_with_config: vec![(
                    valid_mint_tx,
                    invalid_mint_config_tx.clone(),
                    invalid_mint_config_tx.prefix.configs[0].clone(),
                )],
                ..Default::default()
            },
            &root_element,
        );
        assert_eq!(
            result,
            Err(Error::MalformedMintingTx(
                MintValidationError::InvalidSignature
            ))
        );
    }

    #[test_with_logger]
    fn form_block_rejects_duplicate_mint_tx(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);

        let (mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);

        let recipient1 = AccountKey::random(&mut rng);

        let mint_tx1 = create_mint_tx_to_recipient(
            token_id1,
            &signers1,
            12,
            &recipient1.default_subaddress(),
            &mut rng,
        );

        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map = GovernorsMap::try_from_iter([(token_id1, signer_set1)]).unwrap();

        for block_version in BlockVersion::iterator() {
            if !block_version.mint_transactions_are_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                governors_map: governors_map.clone(),
                governors_signature: sign_governors_map(&governors_map),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    mint_txs_with_config: vec![
                        (
                            mint_tx1.clone(),
                            mint_config_tx1.clone(),
                            mint_config_tx1.prefix.configs[0].clone(),
                        ),
                        (
                            mint_tx1.clone(),
                            mint_config_tx1.clone(),
                            mint_config_tx1.prefix.configs[0].clone(),
                        ),
                    ],
                    ..Default::default()
                },
                &root_element,
            );

            if let Err(Error::FormBlock(description)) = form_block_result {
                assert!(description.contains("Duplicate MintTx"));
            } else {
                panic!("Expected FormBlock error, got: {:#?}", form_block_result);
            }
        }
    }

    #[test_with_logger]
    fn form_block_rejects_mint_tx_by_unknown_governor(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);

        let (mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);
        let (_mint_config_tx2, signers2) = create_mint_config_tx_and_signers(token_id2, &mut rng);

        let recipient1 = AccountKey::random(&mut rng);

        let mint_tx1 = create_mint_tx_to_recipient(
            token_id1,
            &signers1,
            12,
            &recipient1.default_subaddress(),
            &mut rng,
        );

        let signer_set2 = SignerSet::new(signers2.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map = GovernorsMap::try_from_iter([
            // NOTE: token_id1 is also governed by signer_set2, which means a MintTx signed with a
            // config that is signed by signer_set1 should get refused.
            (token_id1, signer_set2.clone()),
            (token_id2, signer_set2),
        ])
        .unwrap();

        for block_version in BlockVersion::iterator() {
            if !block_version.mint_transactions_are_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                governors_map: governors_map.clone(),
                governors_signature: sign_governors_map(&governors_map),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    mint_txs_with_config: vec![(
                        mint_tx1.clone(),
                        // This config is not going to get accepted because our governors map
                        // wants different signers.
                        mint_config_tx1.clone(),
                        mint_config_tx1.prefix.configs[0].clone(),
                    )],
                    ..Default::default()
                },
                &root_element,
            );
            assert_eq!(
                form_block_result,
                Err(Error::MalformedMintingTx(
                    MintValidationError::InvalidSignature
                ))
            );
        }
    }

    #[test_with_logger]
    fn form_block_accepts_valid_mint_config_txs(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);

        let (mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);
        let (mint_config_tx2, signers2) = create_mint_config_tx_and_signers(token_id2, &mut rng);

        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);
        let signer_set2 = SignerSet::new(signers2.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map = GovernorsMap::try_from_iter([
            (token_id1, signer_set1.clone()),
            (token_id2, signer_set2.clone()),
        ])
        .unwrap();

        for block_version in BlockVersion::iterator() {
            if !block_version.mint_transactions_are_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                governors_map: governors_map.clone(),
                governors_signature: sign_governors_map(&governors_map),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = mint_config_tx1.prefix.tombstone_block - 1; // Don't want to exceed the tombstone block
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let (block, block_contents, signature) = enclave
                .form_block(
                    &parent_block,
                    FormBlockInputs {
                        mint_config_txs: vec![mint_config_tx1.clone(), mint_config_tx2.clone()],
                        ..Default::default()
                    },
                    &root_element,
                )
                .unwrap();

            // Verify signature
            assert_eq!(
                signature.signer(),
                &enclave
                    .ake
                    .get_identity()
                    .signing_keypair
                    .lock()
                    .unwrap()
                    .public_key()
            );

            assert!(signature.verify(&block).is_ok());

            // The block contents should contain the two mint config txs.
            assert_eq!(
                block_contents.validated_mint_config_txs,
                vec![
                    ValidatedMintConfigTx {
                        mint_config_tx: mint_config_tx1.clone(),
                        signer_set: signer_set1.clone(),
                    },
                    ValidatedMintConfigTx {
                        mint_config_tx: mint_config_tx2.clone(),
                        signer_set: signer_set2.clone(),
                    }
                ]
            );

            // There should be no outputs, key images or mint txs
            assert!(block_contents.outputs.is_empty());
            assert!(block_contents.key_images.is_empty());
            assert!(block_contents.mint_txs.is_empty());
        }
    }

    #[test_with_logger]
    fn form_block_rejects_mint_config_tx_for_unknown_token(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);

        let (mint_config_tx1, _signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);
        let (_mint_config_tx2, signers2) = create_mint_config_tx_and_signers(token_id2, &mut rng);

        let signer_set2 = SignerSet::new(signers2.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map = GovernorsMap::try_from_iter([(token_id2, signer_set2)]).unwrap();

        for block_version in BlockVersion::iterator() {
            if !block_version.mint_transactions_are_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                governors_map: governors_map.clone(),
                governors_signature: sign_governors_map(&governors_map),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    mint_config_txs: vec![mint_config_tx1.clone()],
                    ..Default::default()
                },
                &root_element,
            );

            assert_eq!(
                form_block_result,
                Err(Error::MalformedMintingTx(MintValidationError::NoGovernors(
                    TokenId::from(1)
                )))
            )
        }
    }

    #[test_with_logger]
    fn form_block_rejects_mint_config_tx_with_invalid_signature(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);

        let (mut mint_config_tx1, signers1) =
            create_mint_config_tx_and_signers(token_id1, &mut rng);

        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);

        // This will invalidate the signature.
        mint_config_tx1.prefix.tombstone_block += 1;

        let governors_map = GovernorsMap::try_from_iter([(token_id1, signer_set1)]).unwrap();

        for block_version in BlockVersion::iterator() {
            if !block_version.mint_transactions_are_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                governors_map: governors_map.clone(),
                governors_signature: sign_governors_map(&governors_map),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = mint_config_tx1.prefix.tombstone_block - 1; // Don't want to exceed the tombstone block
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    mint_config_txs: vec![mint_config_tx1.clone()],
                    ..Default::default()
                },
                &root_element,
            );
            assert_eq!(
                form_block_result,
                Err(Error::MalformedMintingTx(
                    MintValidationError::InvalidSignature
                ))
            );
        }
    }

    #[test_with_logger]
    fn form_block_rejects_duplicate_mint_config_tx(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);

        let (mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);

        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map = GovernorsMap::try_from_iter([(token_id1, signer_set1)]).unwrap();

        for block_version in BlockVersion::iterator() {
            if !block_version.mint_transactions_are_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                governors_map: governors_map.clone(),
                governors_signature: sign_governors_map(&governors_map),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = mint_config_tx1.prefix.tombstone_block - 1; // Don't want to exceed the tombstone block
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    mint_config_txs: vec![mint_config_tx1.clone(), mint_config_tx1.clone()],
                    ..Default::default()
                },
                &root_element,
            );
            if let Err(Error::FormBlock(description)) = form_block_result {
                assert!(description.contains("Duplicate MintConfigTx"));
            } else {
                panic!("Expected FormBlock error, got: {:#?}", form_block_result);
            }
        }
    }

    #[test_with_logger]
    fn test_form_block_with_both_regular_outputs_and_mint_txs_works(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);

        let (mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);
        let (mint_config_tx2, signers2) = create_mint_config_tx_and_signers(token_id2, &mut rng);

        let recipient1 = AccountKey::random(&mut rng);
        let recipient2 = AccountKey::random(&mut rng);

        let mint_tx1 = create_mint_tx_to_recipient(
            token_id1,
            &signers1,
            12,
            &recipient1.default_subaddress(),
            &mut rng,
        );
        let mint_tx2 = create_mint_tx_to_recipient(
            token_id2,
            &signers2,
            200,
            &recipient2.default_subaddress(),
            &mut rng,
        );

        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);
        let signer_set2 = SignerSet::new(signers2.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map =
            GovernorsMap::try_from_iter([(token_id1, signer_set1), (token_id2, signer_set2)])
                .unwrap();

        for block_version in BlockVersion::iterator() {
            if !block_version.mint_transactions_are_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                governors_map: governors_map.clone(),
                governors_signature: sign_governors_map(&governors_map),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Create a valid test transaction.
            let sender = AccountKey::random(&mut rng);
            let recipient = AccountKey::random(&mut rng);

            let mut ledger = create_ledger();
            let n_blocks = 1;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Spend outputs from the origin block.
            let origin_block_contents = ledger.get_block_contents(0).unwrap();

            let input_transactions: Vec<Tx> = (0..3)
                .map(|i| {
                    let tx_out = origin_block_contents.outputs[i].clone();

                    create_transaction(
                        block_version,
                        &ledger,
                        &tx_out,
                        &sender,
                        &recipient.default_subaddress(),
                        n_blocks + 1,
                        &mut rng,
                    )
                })
                .collect();

            let total_fee: u64 = input_transactions.iter().map(|tx| tx.prefix.fee).sum();

            // Create WellFormedEncryptedTxs + proofs
            let well_formed_encrypted_txs_with_proofs: Vec<_> = input_transactions
                .iter()
                .map(|tx| {
                    let well_formed_tx = WellFormedTx::from(tx.clone());
                    let encrypted_tx = enclave
                        .encrypt_well_formed_tx(&well_formed_tx, &mut rng)
                        .unwrap();

                    let highest_indices = well_formed_tx.tx.get_membership_proof_highest_indices();
                    let membership_proofs = ledger
                        .get_tx_out_proof_of_memberships(&highest_indices)
                        .expect("failed getting proof");
                    (encrypted_tx, membership_proofs)
                })
                .collect();

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();
            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let (block, block_contents, signature) = enclave
                .form_block(
                    &parent_block,
                    FormBlockInputs {
                        well_formed_encrypted_txs_with_proofs,
                        mint_txs_with_config: vec![
                            (
                                mint_tx1.clone(),
                                mint_config_tx1.clone(),
                                mint_config_tx1.prefix.configs[0].clone(),
                            ),
                            (
                                mint_tx2.clone(),
                                mint_config_tx2.clone(),
                                mint_config_tx2.prefix.configs[0].clone(),
                            ),
                        ],
                        ..Default::default()
                    },
                    &root_element,
                )
                .unwrap();

            // Verify signature.
            {
                assert_eq!(
                    signature.signer(),
                    &enclave
                        .ake
                        .get_identity()
                        .signing_keypair
                        .lock()
                        .unwrap()
                        .public_key()
                );

                assert!(signature.verify(&block).is_ok());
            }

            // `block_contents` should include the aggregate fee and two minted outputs.

            let num_outputs: usize = input_transactions
                .iter()
                .map(|tx| tx.prefix.outputs.len())
                .sum();
            assert_eq!(num_outputs + 3, block_contents.outputs.len());

            // One of the outputs should be the aggregate fee.
            let fee_view_key = RistrettoPrivate::try_from(&FEE_VIEW_PRIVATE_KEY).unwrap();

            let fee_output = block_contents
                .outputs
                .iter()
                .find(|output| output.view_key_match(&fee_view_key).is_ok())
                .unwrap();

            // The value of the aggregate fee should equal the total value of fees in the
            // input transaction.
            let (amount, _) = fee_output.view_key_match(&fee_view_key).unwrap();
            assert_eq!(amount.value, total_fee);
            assert_eq!(amount.token_id, Mob::ID);

            // The block contents should contain the two mint txs.
            assert_eq!(
                block_contents.mint_txs,
                vec![mint_tx1.clone(), mint_tx2.clone()]
            );

            // There should be no mint config txs
            assert!(block_contents.validated_mint_config_txs.is_empty());

            // The block contents should contain the minted tx outs.
            let output1 = block_contents
                .outputs
                .iter()
                .find(|output| output.view_key_match(recipient1.view_private_key()).is_ok())
                .unwrap();
            let (amount, _) = output1
                .view_key_match(recipient1.view_private_key())
                .unwrap();
            assert_eq!(amount.value, 12);
            assert_eq!(amount.token_id, token_id1);

            let output2 = block_contents
                .outputs
                .iter()
                .find(|output| output.view_key_match(recipient2.view_private_key()).is_ok())
                .unwrap();
            let (amount, _) = output2
                .view_key_match(recipient2.view_private_key())
                .unwrap();
            assert_eq!(amount.value, 200);
            assert_eq!(amount.token_id, token_id2);
        }
    }

    #[test_with_logger]
    fn form_block_refuses_mint_config_txs_for_unsupported_block_versions(logger: Logger) {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);

        let (mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);
        let (mint_config_tx2, signers2) = create_mint_config_tx_and_signers(token_id2, &mut rng);

        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);
        let signer_set2 = SignerSet::new(signers2.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map =
            GovernorsMap::try_from_iter([(token_id1, signer_set1), (token_id2, signer_set2)])
                .unwrap();

        for block_version in BlockVersion::iterator() {
            if block_version.mint_transactions_are_supported() {
                continue;
            }

            let enclave = SgxConsensusEnclave::new(logger.clone());
            let blockchain_config = BlockchainConfig {
                block_version,
                governors_map: governors_map.clone(),
                governors_signature: sign_governors_map(&governors_map),
                ..Default::default()
            };
            enclave
                .enclave_init(
                    &Default::default(),
                    &Default::default(),
                    &None,
                    blockchain_config,
                )
                .unwrap();

            // Initialize a ledger.
            let sender = AccountKey::random(&mut rng);
            let mut ledger = create_ledger();
            let n_blocks = 3;
            initialize_ledger(block_version, &mut ledger, n_blocks, &sender, &mut rng);

            // Form block
            let parent_block = ledger.get_block(ledger.num_blocks().unwrap() - 1).unwrap();

            let root_element = ledger.get_root_tx_out_membership_element().unwrap();

            let form_block_result = enclave.form_block(
                &parent_block,
                FormBlockInputs {
                    mint_config_txs: vec![mint_config_tx1.clone(), mint_config_tx2.clone()],
                    ..Default::default()
                },
                &root_element,
            );

            assert_eq!(
                form_block_result,
                Err(Error::MalformedMintingTx(
                    MintValidationError::InvalidBlockVersion(block_version)
                ))
            );
        }
    }

    #[test]
    fn validate_mint_config_txs_checks_for_duplicate_nonces() {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);

        let (mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);
        let (mut mint_config_tx2, _signers2) =
            create_mint_config_tx_and_signers(token_id1, &mut rng);

        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map = GovernorsMap::try_from_iter([
            (token_id1, signer_set1.clone()),
            (token_id2, signer_set1),
        ])
        .unwrap();

        let blockchain_config = BlockchainConfig {
            block_version: BlockVersion::MAX,
            governors_map: governors_map.clone(),
            governors_signature: sign_governors_map(&governors_map),
            ..Default::default()
        };

        // The two mint configs have different nonces for the same token ID.
        // Note that we update the signature since we don't want to fail on it not
        // matching the governors for the different token.
        let signers1_refs = signers1.iter().collect::<Vec<_>>();
        mint_config_tx2.signature =
            sign_mint_config_tx_prefix(&mint_config_tx2.prefix, &signers1_refs);
        assert_ne!(mint_config_tx1.prefix.nonce, mint_config_tx2.prefix.nonce);
        SgxConsensusEnclave::validate_mint_config_txs(
            vec![mint_config_tx1.clone(), mint_config_tx2.clone()],
            None,
            &blockchain_config,
        )
        .expect("Mint config txs should be valid");

        // Changing the nonce of the second mint config to match the first
        // should cause a failure.
        mint_config_tx2
            .prefix
            .nonce
            .clone_from(&mint_config_tx1.prefix.nonce);
        match SgxConsensusEnclave::validate_mint_config_txs(
            vec![mint_config_tx1.clone(), mint_config_tx2.clone()],
            None,
            &blockchain_config,
        ) {
            Err(Error::FormBlock(err_str)) => {
                assert!(err_str.starts_with("Duplicate MintConfigTx nonce for token id 1"));
            }
            result => panic!("Unexpected result: {:?}", result),
        }

        // Same nonce on different token ids should be fine.
        mint_config_tx2.prefix.token_id = *token_id2;
        for mint_config in mint_config_tx2.prefix.configs.iter_mut() {
            mint_config.token_id = mint_config_tx2.prefix.token_id;
        }
        mint_config_tx2.signature =
            sign_mint_config_tx_prefix(&mint_config_tx2.prefix, &signers1_refs);
        SgxConsensusEnclave::validate_mint_config_txs(
            vec![mint_config_tx1, mint_config_tx2.clone()],
            None,
            &blockchain_config,
        )
        .expect("Mint config txs should be valid");
    }

    #[test]
    fn validate_mint_txs_checks_for_duplicate_nonces() {
        let mut rng = Hc128Rng::from_seed([77u8; 32]);

        let token_id1 = TokenId::from(1);
        let token_id2 = TokenId::from(2);

        let (mint_config_tx1, signers1) = create_mint_config_tx_and_signers(token_id1, &mut rng);
        let (mint_config_tx2, signers2) = create_mint_config_tx_and_signers(token_id2, &mut rng);

        let signer_set1 = SignerSet::new(signers1.iter().map(|s| s.public_key()).collect(), 1);
        let signer_set2 = SignerSet::new(signers2.iter().map(|s| s.public_key()).collect(), 1);

        let governors_map =
            GovernorsMap::try_from_iter([(token_id1, signer_set1), (token_id2, signer_set2)])
                .unwrap();

        let blockchain_config = BlockchainConfig {
            block_version: BlockVersion::MAX,
            governors_map: governors_map.clone(),
            governors_signature: sign_governors_map(&governors_map),
            ..Default::default()
        };

        let recipient = AccountKey::random(&mut rng);

        let mint_tx1 = create_mint_tx_to_recipient(
            token_id1,
            &signers1,
            12,
            &recipient.default_subaddress(),
            &mut rng,
        );
        let mut mint_tx2 = create_mint_tx_to_recipient(
            token_id1,
            &signers1,
            200,
            &recipient.default_subaddress(),
            &mut rng,
        );

        // The two mint txs have different nonces for the same token ID.
        assert_ne!(mint_tx1.prefix.nonce, mint_tx2.prefix.nonce);
        SgxConsensusEnclave::validate_mint_txs(
            vec![
                (
                    mint_tx1.clone(),
                    mint_config_tx1.clone(),
                    mint_config_tx1.prefix.configs[0].clone(),
                ),
                (
                    mint_tx2.clone(),
                    mint_config_tx1.clone(),
                    mint_config_tx1.prefix.configs[0].clone(),
                ),
            ],
            0,
            &blockchain_config,
        )
        .expect("Mint txs should be valid");

        // Changing the nonce of the second mint tx to match the first
        // should cause a failure.
        mint_tx2.prefix.nonce.clone_from(&mint_tx1.prefix.nonce);
        match SgxConsensusEnclave::validate_mint_txs(
            vec![
                (
                    mint_tx1.clone(),
                    mint_config_tx1.clone(),
                    mint_config_tx1.prefix.configs[0].clone(),
                ),
                (
                    mint_tx2.clone(),
                    mint_config_tx1.clone(),
                    mint_config_tx1.prefix.configs[0].clone(),
                ),
            ],
            0,
            &blockchain_config,
        ) {
            Err(Error::FormBlock(err_str)) => {
                assert!(err_str.starts_with("Duplicate MintTx nonce for token id 1"));
            }
            result => panic!("Unexpected result: {:?}", result),
        }

        // Same nonce on different token ids should be fine.
        mint_tx2.prefix.token_id = *token_id2;
        mint_tx2.signature =
            sign_mint_tx_prefix(&mint_tx2.prefix, &signers2.iter().collect::<Vec<_>>());
        SgxConsensusEnclave::validate_mint_txs(
            vec![
                (
                    mint_tx1,
                    mint_config_tx1.clone(),
                    mint_config_tx1.prefix.configs[0].clone(),
                ),
                (
                    mint_tx2,
                    mint_config_tx2.clone(),
                    mint_config_tx2.prefix.configs[0].clone(),
                ),
            ],
            0,
            &blockchain_config,
        )
        .expect("Mint txs should be valid");
    }
}
